﻿using Jacobi.Vst.Framework;
using Jacobi.Vst.Core;
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Aleator.Resources;

namespace Aleator.Core
{
    /// <summary>
    /// Phrase class for lead melodies
    /// </summary>
    public class MelodyPhrase : ConcretePhrase
    {
        #region Members

        private int m_melodyOctave = Session.HomeOctave.Value;
        private int m_baselength = 0;
        private int m_largeClusterLength = 0;
        private int m_oldClusterID = 0;
        private int m_previous = 0;

        private int[] m_clusterBeats;
        private int[] m_largeClusterBeats;
        private int[] m_oldClusterBeats;

        private List<MelodyCluster> m_clusters;

        #endregion

        #region Constructors

        /// <summary>
        /// Main constructor
        /// </summary>
        /// <param name="progression">Progression that phrase is being attached to</param>
        /// <param name="melodyphrase">Phrase to use in populating this phrase's note list(s)</param>
        public MelodyPhrase(ConcreteProgression progression, MelodyPhrase melodyphrase = null)
        {
            m_progression = progression;
            Notes = new List<ConcreteNote>();
            m_homeOctaveNotes = new List<ConcreteNote>();
            m_clusters = new List<MelodyCluster>();

            if (melodyphrase == null)
            {
                m_noteDistribution = (Distribution)Enum.ToObject(typeof(Distribution), Session.RndGenerator.Next(2));

                //TODO: Implement Melody multipliers
                foreach (Melody m in progression.Melodies)
                    BuildMelody(m);

                SetTotals();
            }
            else
            {
                m_noteDistribution = melodyphrase.m_noteDistribution;
                CopyToPhrase((ConcretePhrase)melodyphrase);
            }

            Progression.Notes.AddRange(m_notes);
        }

        #endregion

        #region Methods

        /// <summary>
        /// Determine how many notes are in the phrase
        /// </summary>
        /// <param name="sigma">Determines the shape of the bell curve when using normal distribution</param>
        protected override void SetTotals(double sigma = 2.0)
        {
            //Always use continuous for rigid. Allow for a few missing notes (one per measure)

            //TotalBeats is eighths so totalbeats * 2 is 16th
            m_continuous.Beta = Progression.TotalBeats * 2;

            //Totalbeats * .25 is a half
            m_continuous.Alpha = Progression.TotalBeats * .25;

            m_noteTotal = (int)m_continuous.NextDouble();

            m_noteOnMessageNumber = Convert.ToInt16(AppResources.LeadOnMessageID);
            m_noteOffMessageNumber = Convert.ToInt16(AppResources.LeadOffMessageID);

            Generate(4);
        }

        /// <summary>
        /// Generate not implemented in base class yet
        /// </summary>
        /// <param name="resolution"></param>
        protected override void Generate(int resolution)
        {
            m_resolution = resolution;

            int notespercluster = 0;
            int notesperlargecluster = 0;

            int beat = 0;
            int i = 0;

            if (m_baselength > 0)
            {
                notespercluster = (int)(((double)m_baselength / (double)(m_progression.TotalBeats / 2)) * m_noteTotal);
                m_clusterBeats = new int[notespercluster];

                while (i < notespercluster)
                {
                    //Hardcoding to * 2 for bug fix
                    //beat = Session.RndGenerator.Next(m_baselength * m_resolution);
                    beat = Session.RndGenerator.Next(m_baselength * m_resolution * 2);

                    if (!m_clusterBeats.Contains(beat))
                        m_clusterBeats[i++] = beat;
                }
            }

            i = 0;

            if (m_largeClusterLength > 0)
            {
                notesperlargecluster = (int)(((double)m_largeClusterLength / (double)(m_progression.TotalBeats / 2)) * m_noteTotal);
                m_largeClusterBeats = new int[notesperlargecluster];

                while (i < notesperlargecluster)
                {
                    //TODO: This is not being calculated properly
                    beat = Session.RndGenerator.Next(m_largeClusterLength * m_resolution);

                    if (!m_largeClusterBeats.Contains(beat))
                        m_largeClusterBeats[i++] = beat;
                }
            }

            foreach (MelodyCluster mc in m_clusters)
            {
                FillCluster(mc);
                m_previous = mc.Location + (mc.Beats / 2);
            }

            i = m_progression.TotalMeasures - 1;

            Multiply();
            m_notes.RemoveAll(n => n == null);

            AddRests();

            //TODO: Come up with a more elegant solution for hanging notes.
            //this.Notes.RemoveAll(n => n.OffMeasure > m_progression.TotalMeasures - 1);
            //this.Notes.RemoveAll(n => n.OffMeasure == n.Measure && n.OffLocation == n.Location);
        }

        private void BuildMelody(Melody m)
        {
            m_clusterCount += m.Cluster.Length;
            m_totalBeats += m.TotalBeats;

            //Beats are coming in as 8th notes - divide in half to compare to numerator
            List<MelodyCluster> smallclusters = m.Cluster.Where(cl => (cl.Beats / 2) < m_progression.Numerator).ToList();
            List<MelodyCluster> largeclusters = m.Cluster.Where(cl => (cl.Beats / 2) > m_progression.Numerator).ToList();

            int beatsum;

            MelodyCluster mcl = new MelodyCluster();

            //If any clusters were less than a measure long and there was only one distinct beatlength, set baselength equal to that / 2
            if (smallclusters.Any() && smallclusters.Select(sc => sc.Beats).Distinct().Count() == 1)
                m_baselength = smallclusters.Select(sc => sc.Beats / 2).First();
            /*
            else if (smallclusters.Any())
            {
                m_baselength = smallclusters.OrderBy(sc => sc.Beats).Select(sc => sc.Beats / 2).First();
                if (m_baselength < 2) { m_baselength = 2; }
            }
            */
            else
            {
                m_baselength = m_progression.Numerator * 2;
                beatsum = m.Cluster.Select(cl => (int)cl.Beats).Sum();

                if (m.Cluster.Count() > 1 && beatsum % m_baselength == 0)
                {
                    mcl.ClusterID = 1;
                    mcl.Beats = (byte)(m_progression.Numerator * (byte)2);
                    mcl.Location = m.Cluster[0].Location;
                    mcl.Multiplier = 1;
                    mcl.BeatDictionary = new Dictionary<int, string>();

                    //TODO: This has been altered to account for melody XML in 8ths. Test.
                    foreach (MelodyCluster mc in m.Cluster)
                        mcl.BeatDictionary.Add(mc.Location - mcl.Location, mc.Notes);
                }
            }

            //If any clusters were more than a measure long store the length of the shortest one, in quarter notes
            if (largeclusters.Any())
                m_largeClusterLength = largeclusters.OrderBy(lc => lc.Beats).Select(lc => lc.Beats / 2).First();

            //Changed this 10/24 - run tomorrow
            if (m.Multiplier == 0)
                m.Multiplier = (byte)(m_progression.TotalBeats / m.TotalBeats);

            //TODO:Melody multiplier must update location
            for (int cl_mult = 0; cl_mult < m.Multiplier; cl_mult++)
            {
                if (cl_mult > 0)
                {
                    BuildCluster(mcl, m, cl_mult);
                }
                else
                {
                    if (mcl.BeatDictionary != null)
                        m_clusters.Add(mcl);
                    else
                        m_clusters.AddRange(m.Cluster);
                }
            }
        }

        private void BuildCluster(MelodyCluster mcl, Melody m, int multiplier)
        {
            if (mcl.BeatDictionary != null)
            {
                MelodyCluster cluster = (MelodyCluster)mcl.Clone();
                cluster.Location += (int)(m.TotalBeats / m.Multiplier) * multiplier;
                m_clusters.Add(cluster);
            }
            else
            {
                List<MelodyCluster> clusters = new List<MelodyCluster>();

                foreach (MelodyCluster mc in m.Cluster)
                {
                    MelodyCluster cluster = (MelodyCluster)mc.Clone();
                    cluster.Location += (int)(m.TotalBeats / m.Multiplier) * multiplier;
                    clusters.Add(cluster);
                }

                m_clusters.AddRange(clusters);
            }
        }

        private void FillCluster(MelodyCluster mc)
        {
            //TODO: Pretty sure mc.Location should be halved here
            int measure = (int)((mc.Location / 2) / m_progression.Numerator);
            int interval = 0;
            int location = 0;

            string[] notearray = null;
            int[] clusterbeats;

            ConcreteNote n;

            if (m_oldClusterID != mc.ClusterID)
            {
                //Beats are coming in as eigths so need to divide by 2
                //If this is a large cluster
                if (mc.Beats / 2 > m_progression.Numerator)
                    clusterbeats = m_largeClusterBeats;
                //If this is a normal cluster
                else if (mc.Beats == m_baselength)
                    clusterbeats = m_clusterBeats;
                //If this is a small cluster
                else
                {
                    //Figure out how the total cluster beats
                    clusterbeats = new int[m_clusterBeats.Count() * (mc.Beats / m_baselength)];

                    //For every repetition of the baselength
                    for (int cb = 0; cb < mc.Beats / m_baselength; cb++)
                    {
                        //For every beat in the clusterbeats list
                        for (int icb = 0; icb < m_clusterBeats.Count(); icb++)
                        {
                            //tmp variable is equal to the quarter value times the (baselength times the resolution), plus the beat value
                            int tmp = (cb * (m_baselength * m_resolution)) + m_clusterBeats[icb];

                            if (tmp * m_resolution <= PlayableNotesInMeasure)
                                clusterbeats[(cb * m_clusterBeats.Count()) + icb] = tmp;
                            else
                                clusterbeats[(cb * m_clusterBeats.Count()) + icb] = tmp % PlayableNotesInMeasure;
                        }
                    }
                }
            }
            else
                clusterbeats = m_oldClusterBeats;

            //TODO: Add logic for multiple melody measure calculation
            for (int mult = 0; mult < mc.Multiplier; mult++)
            {
                if (mult > 0)
                    mc.Location = mult * mc.Beats + m_previous;

                foreach (int beatnum in clusterbeats)
                {
                    //Location will be reflective of eighth notes here so dive by 2
                    measure = (int)((mc.Location / 2) / m_progression.Numerator);

                    if (beatnum >= (m_progression.Numerator * m_resolution))
                    {
                        measure += (int)(beatnum / (m_progression.Numerator * m_resolution));

                        //Bug fix here
                        if (beatnum >= (measure * m_progression.Numerator * m_resolution))
                            location = ((mc.Location % m_progression.Numerator) * m_resolution) + beatnum - (measure * m_progression.Numerator * m_resolution);
                        else
                            location = ((mc.Location % m_progression.Numerator) * m_resolution) + beatnum - (m_progression.Numerator * m_resolution);

                        //Dupe check
                        //if (m_notes.Where(note => note.Measure == measure && note.Location == location).Any())
                        //{
                        //    Debug.WriteLine("Duplicate detected!");
                        //}
                    }
                    else if (measure > 0)
                    {
                        measure += (int)((mc.Location + beatnum) / 2 - (measure * m_progression.Numerator)) / m_progression.Numerator;

                        //TODO: this location is not being calculated properly
                        location = (((mc.Location / 2) % m_progression.Numerator) * m_resolution) + beatnum;

                        if (location > PlayableNotesInMeasure)
                            location = location % PlayableNotesInMeasure;

                        //Dupe check
                        //if (m_notes.Where(note => note.Measure == measure && note.Location == location).Any())
                        //{
                        //    Debug.WriteLine("Duplicate detected!");
                        //}
                    }
                    else
                    {
                        location = (mc.Location * m_resolution) + beatnum;

                        if (location > PlayableNotesInMeasure)
                            location = location % PlayableNotesInMeasure;

                        //Dupe check
                        //if (m_notes.Where(note => note.Measure == measure && note.Location == location).Any())
                        //{
                        //    Debug.WriteLine("Duplicate detected!");
                        //}
                    }

                    if (mc.BeatDictionary == null)
                        notearray = mc.Notes.Split(',');
                    else
                        notearray = mc.BeatDictionary.Where(bd => bd.Key <= location).OrderByDescending(bd => bd.Key)
                            .Select(bd => bd.Value).First().Split(',');

                    interval = Convert.ToInt32(notearray[Session.RndGenerator.Next(notearray.Length)]);

                    try
                    {
                        if (interval < 1)
                            n = CircleOfFifths.Notes.Where(no => no.Octave == m_melodyOctave - 1 && no.Interval == interval + 6).First();
                        else if (interval > 7)
                            n = CircleOfFifths.Notes.Where(no => no.Octave == m_melodyOctave + 1 && no.Interval == interval - 8).First();
                        else
                            n = CircleOfFifths.Notes.Where(no => no.Octave == m_melodyOctave && no.Interval == interval - 1).First();
                    }
                    catch (InvalidOperationException)
                    {
                        if (interval < 1)
                            n = CircleOfFifths.Notes.Where(no => no.Octave == m_melodyOctave && no.Interval == interval + 6).First();
                        else
                            n = CircleOfFifths.Notes.Where(no => no.Octave == m_melodyOctave && no.Interval == interval - 8).First();
                    }

                    //TODO: eliminate dupes later
                    if (!m_notes.Where(note => note.Measure == measure && note.Location == location).Any())
                    {
                        Debug.WriteLine(string.Format("Adding melody note at measure {0}, location {1}", measure, location));
                        m_notes.Add((ConcreteNote)m_notefactory.GetNote(location, measure, n, m_noteOnMessageNumber, m_noteOffMessageNumber, this));
                    }
                    else
                    {
                        Debug.WriteLine("Duplicate detected!");
                    }
                }
            }

            m_oldClusterID = mc.ClusterID;
            m_oldClusterBeats = clusterbeats;
        }

        private void AddRemainingNote(MelodyCluster mc, int measure, int location)
        {
            ConcreteNote n = null;

            string[] notearray = mc.Notes.Split(',');
            int interval = Convert.ToInt32(notearray[Session.RndGenerator.Next(notearray.Length)]);

            if (interval < 1)
                n = CircleOfFifths.Notes.Where(no => no.Octave == m_melodyOctave - 1 && no.Interval == interval + 7).First();
            else if (interval > 7)
                n = CircleOfFifths.Notes.Where(no => no.Octave == m_melodyOctave + 1 && no.Interval == interval - 8).First();
            else
                n = CircleOfFifths.Notes.Where(no => no.Octave == m_melodyOctave && no.Interval == interval - 1).First();

            m_notes.Add((ConcreteNote)m_notefactory.GetNote(location, measure, n, m_noteOnMessageNumber, m_noteOffMessageNumber, this));
        }

        private void Multiply()
        {
            int mult = (int)(m_progression.TotalBeats / m_totalBeats);
            //Remember that 2 is hardcoded here to account for 16ths. This needs to be fixed.
            int measurespermelody = (int)((m_totalBeats / 2) / m_progression.Numerator);

            ConcreteNote[] originalarray = new ConcreteNote[m_notes.Count];

            m_notes.CopyTo(originalarray);

            for (int i = 1; i < mult; i++)
            {
                foreach (ConcreteNote n in originalarray)
                {
                    Debug.WriteLine(string.Format("Adding melody note at measure {0}, location {1}", (measurespermelody * i) + n.Measure, n.Location));
                    m_notes.Add((ConcreteNote)m_notefactory.GetNote(n.Location, (measurespermelody * i) + n.Measure, n, m_noteOnMessageNumber,
                        m_noteOffMessageNumber, this));
                }
            }
        }

        #endregion
    }
}
